/****************************************************************************************************************************
*																															*
* Originaly written by Andrew Pennington (I think)																			*		
*																															*
* Re-Written by Ross Smith - Jan 2005																						* 
* Re-written to remove the floating point errors that occured due to the scene graph design. Previously the hand			*
* and the users view flew around the world by adding an offset to the origin of the polhemus transmitter.  The new			*
* structure moves the world around the users hand and head (view) so both the hand and head are always relative to the		*
* polhemus transmitter.																										*																													*
*																															*
****************************************************************************************************************************/

import java.applet.Applet;
import java.awt.*;
import com.sun.j3d.utils.applet.MainFrame;
import com.sun.j3d.utils.universe.*;
import javax.media.j3d.*;
import javax.vecmath.*;
import java.awt.event.*;
import java.awt.BorderLayout;
import java.awt.GraphicsConfiguration;
import java.util.HashMap;

import com.sun.j3d.utils.geometry.*;

/* world imports */
import tracker.TrackerFastrak;
import tracker.TrackerPolhemus;
import tracker.TrackerSerialPort;
import tracker.TrackerThread;
import world.geometry.Clock;
import world.geometry.Goblet;
import world.geometry.Room;
import world.ClockBehavior;


public class WorldMain extends Applet {
	
	/** the default width of the window when not in fullscreen mode */
	private static final int WINDOW_WIDTH = 780;
	
	/** the default height of the window when not in fullscreen mode */
	private static final int WINDOW_HEIGHT = 540;

	/** run the demo in fullscreen mode */
	private static boolean isFullScreen = false;

	private static boolean kbdViewControl = false;
	
	/** Enable/Disable debugging output */
	private static boolean DEBUG = false;
	
	/*Geomety Objects*/
    private Hand hand1;
    private Hand hand2;
   
    /*Java universe*/
    private SimpleUniverse u;
		
	/** the transform group containing the world. This is moved around the viewing platform
	 *  to simulate movement. */
	private TransformGroup worldBranch;

	/** the transform group containing the groups that appear to move with the user (ie. the hands).
	 * These remain static while the world moves around the viewing platform. */
	private BranchGroup staticBranch;
    

	public BranchGroup createSceneGraph() {

		if (DEBUG)
			System.out.println("Creating Scene Graph");

		/*
		 *               root
		 *                |
		 *      +-------------------+
		 *      |                   |
		 *  worldBranch         staticBranch
		 *     /|\                 /\
		 *(world objects)      (user hands)
		 */

		/*Create the root of the branch graph and the root level transform group */
		BranchGroup root = new BranchGroup();

		/* Add all the world geometry to the world branch */
		worldBranch = new TransformGroup();
		worldBranch.setCapability(TransformGroup.ALLOW_TRANSFORM_READ);
		worldBranch.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE);
		worldBranch.setCapability(TransformGroup.ALLOW_CHILDREN_EXTEND);
		root.addChild(worldBranch);
		
		staticBranch = new BranchGroup();
		staticBranch.setCapability(BranchGroup.ALLOW_CHILDREN_EXTEND);
		root.addChild(staticBranch);
	
		log("Finished building scene graph");

		return root;
	}

    
    /*
     * create your world in this method
	 */
    public void createWorld(TransformGroup world){

        /*Create a bounds for the background and lights and behaviors*/
        BoundingSphere bounds = new BoundingSphere(new Point3d(0.0,0.0,0.0), 200.0);
      
        log("Adding global lights");
        
        /* Set up the global lights */
        Color3f lColor1 = new Color3f(0.9f, 0.9f, 0.9f);
        Vector3f lDir1  = new Vector3f(0f, -0.5f, -1.0f);
        Vector3f lDir2  = new Vector3f(1.0f, -1.0f, 1.0f);
        Color3f alColor = new Color3f(0.4f, 0.4f, 0.5f);
        AmbientLight aLgt = new AmbientLight(alColor);
        aLgt.setInfluencingBounds(bounds);
        DirectionalLight lgt1 = new DirectionalLight(lColor1, lDir1);
		DirectionalLight lgt2 = new DirectionalLight(lColor1, lDir2);
		lgt1.setInfluencingBounds(bounds);
        lgt2.setInfluencingBounds(bounds);
        aLgt.setEnable(true);
        lgt1.setEnable(true);
        lgt2.setEnable(true);
        world.addChild(aLgt);
        world.addChild(lgt1);
        world.addChild(lgt2);
      
        /*Add the walls (just a box we live in)*/
        Room room = new Room ();
        world.addChild (room);
        setCollidable (room);
      
        // add a VRML object (corner pillars for the room)
        VRMLObject pillars = new VRMLObject("room");
        world.addChild(pillars);
        setCollidable(pillars);
        
        // add another VRML object (ball on a stand)
        VRMLObject ball1 = new VRMLObject ("ball", new Vector3d(5.0, -2.0, -5.0));
        world.addChild(ball1);
        setCollidable(ball1);
      
        // add another VRML object (another ball on a stand)
		VRMLObject ball2 = new VRMLObject("ball", new Vector3d(-5.0, -2.0, -5.0));
		world.addChild(ball2);
		setCollidable(ball2);
      
        // add another VRML object (a coffee table)
        VRMLObject table = new VRMLObject ("coffee_table", new Vector3d (1.0, -2.0, -9.0));
        world.addChild (table);
        setCollidable (table);
      
        // add another VRML object (a bookcase)
        VRMLObject bookcase = new VRMLObject ("bookcase", new Vector3d(-3.0, -0.9, -9.99));
        world.addChild(bookcase);
        setCollidable(bookcase);
      
        // add a clock
        Clock clock_ = new Clock ();
        TransformGroup clockTransformGroup = new TransformGroup ();
        clockTransformGroup.setUserData("Transform");
        Transform3D clockTransform = new Transform3D();
        Vector3d clockLocation = new Vector3d(-3.0, 0.3, -9.9);
        clockTransform.setTranslation(clockLocation);
        clockTransformGroup.setTransform(clockTransform);
        clockTransformGroup.addChild(clock_);
        world.addChild(clockTransformGroup);
        setCollidable(clockTransformGroup);
		
		ClockBehavior cb = new ClockBehavior (clock_);
        cb.setSchedulingBounds (new BoundingSphere(new Point3d (0.0, 0.0, 0.0), Float.MAX_VALUE));
        world.addChild(cb);
		
        /*Add goblet*/
        Goblet goblet1_ = new Goblet ();
        TransformGroup goblet1TransformGroup = new TransformGroup ();
        goblet1TransformGroup.setUserData("Transform");
        Transform3D goblet1Transform = new Transform3D();
        Vector3d goblet1Location = new Vector3d(1.2, -1.3, -9.3);
        goblet1Transform.rotZ(Math.PI / 2.0);
        goblet1Transform.setTranslation(goblet1Location);
        goblet1Transform.setScale(0.3);
        goblet1TransformGroup.setTransform(goblet1Transform);
        goblet1TransformGroup.addChild(goblet1_);
        world.addChild(goblet1TransformGroup);
        setCollidable(goblet1TransformGroup);
      
        // add another goblet
        Goblet goblet2_ = new Goblet ();
        TransformGroup goblet2TransformGroup = new TransformGroup ();
        goblet2TransformGroup.setUserData ("Transform");
        Transform3D goblet2Transform = new Transform3D ();
        Vector3d goblet2Location = new Vector3d (0.4, -1.3, -9.15);
        goblet2Transform.rotZ (3.14159/2.0);
        goblet2Transform.setTranslation (goblet2Location);
        goblet2Transform.setScale (0.3);
        goblet2TransformGroup.setTransform (goblet2Transform);
        goblet2TransformGroup.addChild (goblet2_);
        world.addChild(goblet2TransformGroup);
        //setCollidable(goblet2TransformGroup);
        
        log("Finished building scene graph");
        
    }
    
    
    public WorldMain() {
      
    	setLayout(new BorderLayout());
        GraphicsConfiguration config = SimpleUniverse.getPreferredConfiguration();

        FixedCanvas3D c = new FixedCanvas3D(config);
        add("Center", c);
        
        // Create a simple scene and attach it to the virtual universe
        BranchGroup scene = createSceneGraph();
        createWorld(worldBranch);
        u = new SimpleUniverse(c);
      
        TransformGroup tg = u.getViewingPlatform ().getViewPlatformTransform ();
        Transform3D tf = new Transform3D ();
      
        // set the initial viewing position
        Point3d eye = new Point3d();
        Point3d centre = new Point3d (0, 0, -1.0);
        Vector3d up = new Vector3d (0, 1.5, 0);
        tf.lookAt(eye, centre, up); // just here to show how
        tf.invert();
        tg.setTransform(tf);
         
        View view = u.getViewer().getView();
        
        view.setBackClipDistance(100.0);
        view.setFrontClipDistance(0.01);
        view.setFieldOfView(1.0);
        int policy = view.getViewPolicy();

        /* log some debug messages */
        log("frontClip distance = " + view.getFrontClipDistance());
        log("fov = " + view.getFieldOfView());
        log("Compatibility Mode Enabled: " + view.getCompatibilityModeEnable());
        log("View policy: " + (policy == View.SCREEN_VIEW ? "SCREEN_VIEW" : "HMD_VIEW"));
           
        PhysicalEnvironment pe = u.getViewer ().getPhysicalEnvironment ();
        PhysicalBody pb = u.getViewer().getPhysicalBody();
      	ViewingPlatform vp = u.getViewingPlatform();
        TransformGroup viewPlatformTrans = u.getViewingPlatform().getViewPlatformTransform();
      
        /*Add text to the overlay canvas to display the current frame rate*/
        Text2D fpsText = new Text2D("FPS = 00", new Color3f(0.4f,0.8f, 0), "", 28, Font.PLAIN);
        fpsText.setCapability(Text2D.ALLOW_APPEARANCE_READ);
        fpsText.setCapability(Text2D.ALLOW_APPEARANCE_WRITE);
        fpsText.getAppearance().setCapability(Appearance.ALLOW_TEXTURE_READ);
        fpsText.getAppearance().setCapability(Appearance.ALLOW_TEXTURE_WRITE);
        TransformGroup overlay = new TransformGroup();
        Transform3D overlaytrans = new Transform3D();
        overlaytrans.set(new Vector3d(50, 50, 0)); // in pixel coordinates
        overlaytrans.setScale(256); // account for the scaling in fpsText
        overlay.setTransform(overlaytrans);
        overlay.addChild(fpsText);

        /*Add text to the overlay canvas to display the current frame rate*/
        Text2D polhemusFpsText = new Text2D("Polhemus FPS = Not Attached", new Color3f(0.5f,0.8f,.0f), "", 18, Font.PLAIN);
        polhemusFpsText.setCapability(Text2D.ALLOW_APPEARANCE_READ);
        polhemusFpsText.setCapability(Text2D.ALLOW_APPEARANCE_WRITE);
        polhemusFpsText.getAppearance().setCapability(Appearance.ALLOW_TEXTURE_READ);
        polhemusFpsText.getAppearance().setCapability(Appearance.ALLOW_TEXTURE_WRITE);
        TransformGroup overlay1 = new TransformGroup();
        Transform3D overlaytrans1 = new Transform3D();
        overlaytrans1.set(new Vector3d(50, 20, 0)); // in pixel coordinates
        overlaytrans1.setScale(256); // account for the scaling in Text2D
        overlay1.setTransform(overlaytrans1);
        overlay1.addChild(polhemusFpsText);
        
        /*Add the overlay group - Columbia University Classes*/
		final OverlayGroup og = new OverlayGroup();
		BranchGroup obg = new BranchGroup();
		obg.addChild(overlay);
		obg.addChild(overlay1);
		og.setupOverlayGroup(c, viewPlatformTrans, obg);
		og.setOnNearClippingPlane(true);

		
		/* Add the hands to the static branch of the universe. When moving, the
		 * hands and viewing platform remain static while the world branch moves around us */
		hand1 = new Hand(); // right hand
        setCollidable(hand1);
        staticBranch.addChild(hand1);
      	
        
        TrackerThread trackerThread = null;
        ButtonDevice buttonController;

        if (kbdViewControl){
         	
            System.out.println ("Keyboard Control Enabled");
            
        	buttonController = ButtonDevice.getKeyboardDeviceForAWTComponent(u.getViewer().getCanvas3Ds()[0]);
        	
        } else {

         	/*Use the Polhemus tracker to move around the world*/
         	System.out.println("Polhemus Tracker mode Enabled");
         	
            TrackerSerialPort sp = new TrackerSerialPort();
            TrackerPolhemus tracker = sp.getPolhemusTracker();
            System.out.println("Using " + tracker.getTrackerType() + " Polhemus tracker");
            
            /* Java virtual input device */
            trackerThread = new TrackerThread(tracker, sp);
            trackerThread.setProcessingMode(InputDevice.NON_BLOCKING);
            trackerThread.initialize();
            trackerThread.start();
            u.getViewer().getPhysicalEnvironment().addInputDevice(trackerThread);
            
            /*Add the hand sensor to the java concept of tracking*/
            pe.addInputDevice(trackerThread);     
            pe.setSensor(0, trackerThread.getSensor(0));
            pe.setHeadIndex(0);
            pe.setSensor(1, trackerThread.getSensor(1));

            buttonController = ButtonDevice.getMouseDeviceForTracker(tracker);
            
            /*If we are using the Fastrak (the two-handed tracker) then add another hand */
            if (tracker instanceof TrackerFastrak){

            	hand2 = new Hand(); /* left hand */
	            setCollidable(hand2);
	            staticBranch.addChild(hand2);
	          	
	            /*Add a behaviour to control the hand with the second sensor of the Polhemus Tracker*/
	            HandBehaviorSecondary h2 = new HandBehaviorSecondary(hand2, trackerThread.getSensor(2), 
	            		buttonController, viewPlatformTrans, scene, worldBranch);
	            h2.setSchedulingBounds(new BoundingSphere(new Point3d(), 20));
	            scene.addChild(h2);
	            pe.setSensor(2, trackerThread.getSensor(2));
            }
            
        }
        
        /*Add a behaviour to control the hand with the second sensor of the Polhemus Tracker*/
        HandBehaviorPrimary h1 = new HandBehaviorPrimary(hand1, trackerThread != null ? trackerThread.getSensor(1) : null, 
        		buttonController, viewPlatformTrans, scene, worldBranch);
        h1.setSchedulingBounds(new BoundingSphere(new Point3d(), 20));
        scene.addChild(h1);
        
        /* add the behavior to control viewing direction */
        //HeadBehavior head = new HeadBehavior(tg, pe.getSensor(0));
        HeadBehavior head;
        if (kbdViewControl)
        	head = new KeyboardHeadBehavior(tg, (KeyboardDevice)buttonController);
        else
        	head = new TrackerHeadBehavior(tg, pe.getSensor(0));
        head.setSchedulingBounds(new BoundingSphere(new Point3d(), 20));
     	scene.addChild(head);
       
        // HeadBehavior head = new HeadBehavior(tg, pe.getSensor(0), polhemusFpsText, og, trackerThread);
		FPSBehavior fps = new FPSBehavior(fpsText, polhemusFpsText, null, og);
		scene.addChild(fps);
         
        /*Add a key listener so we can exit when esc, q of Q are pressed*/
     	Canvas3D[] canvases;
    	canvases = u.getViewer().getCanvas3Ds();

    	class QuitListener extends KeyAdapter {
    	    public void keyTyped(KeyEvent e) {
    	    	char c = e.getKeyChar();
    	    	if (c == 'q' || c == 'Q' || c == 27){
    	    		System.out.println("VR Virtual World Terminated");
    	    		System.exit(0);
    	    	}
    	    }
    	}

    	QuitListener quitListener = new QuitListener();
    	for (int i = 0; i < canvases.length; i++)
    	    canvases[i].addKeyListener(quitListener);

    	view.setMinimumFrameCycleTime(0);
    	scene.compile();
        u.addBranchGraph(scene);
         
        log("Java3D Sensor Details:\n\tPolhemus Sensor 1: " + pe.getSensor(0) + "\n\tPolhemus Sensor 2: " + pe.getSensor(0) + "\n\tPolhemus Sensor 3: " + pe.getSensor(2));
      }
   
	
	 /** sets an object to be collidable for hand interaction at the geometry
	  *  level. These computations are far slower but more accurate than a
	  *  bounding box method. */
	 public void setCollidable (Node node) {
	 	 node.setCapability(Node.ENABLE_PICK_REPORTING);
	 	 node.setCapability(Node.ENABLE_COLLISION_REPORTING);
	 	 node.setCapability(Node.ALLOW_BOUNDS_READ);
	 	 node.setCapability(Node.ALLOW_BOUNDS_WRITE);
	 	 node.setCapability(Node.ALLOW_PICKABLE_READ);
	 	 node.setCapability(Node.ALLOW_PICKABLE_WRITE);
	 	 node.setCapability(Node.ALLOW_COLLIDABLE_READ);
	 	 node.setCapability(Node.ALLOW_COLLIDABLE_WRITE);
	 	 node.setCapability(Node.ALLOW_AUTO_COMPUTE_BOUNDS_READ);
	 	 node.setCapability(Node.ALLOW_AUTO_COMPUTE_BOUNDS_WRITE);
	 	 node.setCapability(Node.ALLOW_LOCAL_TO_VWORLD_READ);
    }
	 
	 public void destroy() {
    	u.cleanup();
    }

    /** send the display to fullscreen mode. Taken from 
     * http://javaalmanac.com/egs/java.awt/screen_FullWin.html?l=rel */
    private static void enterFullscreen(Window win){
        GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
        GraphicsDevice gs = ge.getDefaultScreenDevice();
        if (gs.isFullScreenSupported()) {
            System.out.println("Fullscreen mode is supported.");
        } else {
            System.out.println("Fullscreen mode is emulated.");
        }
		gs.setFullScreenWindow(win);
		win.validate();
    }
    
    private static void exitFullscreen(){
        GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
        GraphicsDevice gs = ge.getDefaultScreenDevice();
        gs.setFullScreenWindow(null);
    }
    
    
    public static void log(String message){
    	if (DEBUG)
    		System.out.println(message);
    }
    
    public static void main(String[] args) {

    	if (args.length > 0){
			for (int i = 0; i < args.length; i++){
				if (args[i].equalsIgnoreCase("-keyboard"))
					kbdViewControl = true;
				if (args[i].equalsIgnoreCase("-fullscreen"))
					isFullScreen = true;
			}
		}
		
    	/*Create the world*/
    	WorldMain demoWorld = new WorldMain();
    	
    	/*Create the main frame our world will be contained within*/
    	MainFrame mf = new MainFrame(demoWorld, WorldMain.WINDOW_WIDTH, WorldMain.WINDOW_HEIGHT);
    	if (isFullScreen)
    		enterFullscreen(mf);
    }
}
